#pragma once

#include "NamedPipeServer.h"
#include <vector>

/*
Name = \\.\\pipe\\DInput-Hook
\\.
	Local computer.

\\pipe\\
	Not a physical file path, but rather a pipe

DInput-Hook
	Pipe name.
*/
const std::string KEYBDPIPENAME = _T("\\\\.\\pipe\\DxKeybdPipe");

/*
Class: KeyboardDevice
	This class implements the interface IDirectInputDevice8 and represents the core of the hooking procedure,
	together with <NamedPipeServer>.

	On creation, this class creates a <NamedPipeServer> with a pipe named "DInput-Hook". The pipe accepts
	messages in the form of strings. Each message (string) is a space-delimited list of scan-codes.
	The keys with the corresponding scan-codes will appear pressed to the hooked application the next time
	it retrieves the device state (in immediate mode) via <GetDeviceState>.

	If a malformed message is received, no action is taken - there's no error reporting or anything.
	You might want to implement a more sophisticated communication protocol to suit your needs.

	The class only works in immediate mode. I didn't implement anything for buffered input because you need
	to define the 'semantics' first - i.e. how would you like your injected scancodes interact with genuine
	user-input ones. Totally override? Come before? Come after?

	The bulk of functionality, thus, lies in <GetDeviceState>. Check it out.

Sidenote:
	Typing out all these functions and their parameters literally WRECKED my fingers. My right-hand is in
	pain. This is about the only thing I hate about hooking!
*/
class KeyboardDevice : public IDirectInputDevice8
{
public:
	KeyboardDevice(IDirectInputDevice8* device) : m_device(device), m_serverPipe(new NamedPipeServer(KEYBDPIPENAME))
	{			
		//m_device = new decltype(m_device)(device);
		//m_serverPipe=new decltype(m_serverPipe)(FULL_PIPE_NAME);
		//MessageBoxA(0,"OAOKeyBD!!","WoW",MB_OK);
	}

	/*** IUnknown methods ***/
	STDMETHOD(QueryInterface)(REFIID riid, void** ppvObj)
	{
		return m_device->QueryInterface(riid, ppvObj);
	}

	STDMETHOD_(ULONG,AddRef)()
	{
		return m_device->AddRef();
	}

	STDMETHOD_(ULONG,Release)()
	{
		ULONG count = m_device->Release();
		if(0 == count)
			delete this;

		return count;
	}

	/*** IDirectInputDevice8 methods ***/
	STDMETHOD(GetCapabilities)(LPDIDEVCAPS devCaps)
	{
		return m_device->GetCapabilities(devCaps);
	}

	STDMETHOD(EnumObjects)(LPDIENUMDEVICEOBJECTSCALLBACK callback, LPVOID ref, DWORD flags)	
	{
		return m_device->EnumObjects(callback, ref, flags);
	}

	STDMETHOD(GetProperty)(REFGUID rguid, LPDIPROPHEADER ph)
	{
		return m_device->GetProperty(rguid, ph);
	}

	STDMETHOD(SetProperty)(REFGUID rguid, LPCDIPROPHEADER ph)
	{		
		return m_device->SetProperty(rguid, ph);

		/*
		You can detect immediate/buffered modes as such:
		
		HRESULT hr = m_device->SetProperty(rguid, ph);

		if(SUCCEEDED(hr) && rguid == DIPROP_BUFFERSIZE)
		{
			DWORD data = *reinterpret_cast<const DWORD*>(ph + 1);
			m_is_immediate = (data == 0);
		}

		return hr;
		*/
	}

	STDMETHOD(Acquire)()
	{
		 HRESULT hr=m_device->Acquire();
	//	 return DI_OK;
		 return hr;
	}

	STDMETHOD(Unacquire)()
	{
		return m_device->Unacquire();
	}

					

	STDMETHOD(GetDeviceState)(DWORD size, LPVOID data)
	{
		HRESULT hr = m_device->GetDeviceState(size, data);
		static BOOL mKeybdClicked=FALSE;
		BOOL DataReceived=FALSE;

		static BOOL MacroEnabled=FALSE;		
		static std::vector<int> KeyGroup1;
		static std::vector<int> KeyGroup2;
		static int delay1;
		static int delay2;
		static int MacroDelayCounter;


		
		//message format:
		// Macro mode (precise mode)
		// 'M' 'r' "first key group" 'number of requests' "second key group" 'number of requests' 
		// ex: M r ABC 100 A 200
		// to start macro 'M' '1'
		//		==>start macro looping
		// ex: M 1
		// to stop macro 'M' '0'
		// ex: M 0
		//		==> stop macro looping


		// Normal mode (instant mode)
		// 'N' "key group"
		// ex: N ABC
		//		==> immediate press DI_SOMEKEY which value equals 'A' 'B' 'C' (3keys)
		
		if( size == sizeof(BYTE) * 256){ // it is keyboard
			BYTE* keys = static_cast<BYTE*>(data);
			if(mKeybdClicked){
				mKeybdClicked=FALSE;
				DataReceived=TRUE;
			}else if(m_serverPipe->IsDataAvailable())
			{
				std::string message;
				if(m_serverPipe->Read(message)){					
					std::stringstream ss(message);
					char opt;
					ss>>opt;
					switch(opt){
						case 'M':
							{
								char macroopt;
								ss>>macroopt;
								switch(macroopt){
									case 'r': 
										{
											KeyGroup1.clear();
											KeyGroup2.clear();

											int numKeyElements;
											//keygroup1
											ss>>numKeyElements;
											while(numKeyElements--){
												int thiskey;
												ss>>thiskey;
												KeyGroup1.push_back(thiskey);
											}
											ss>> delay1;
											//keygroup2
											ss>>numKeyElements;
											while(numKeyElements--){
												int thiskey;
												ss>>thiskey;
												KeyGroup2.push_back(thiskey);
											}
											ss>> delay2;
											break;

										}
									case '0':
										MacroEnabled=FALSE;										
										break;
									case '1':
										MacroEnabled=TRUE;									
										MacroDelayCounter=0;
										break;
									default:
										break;
								}
							}
							break;
						case 'N':
							{
								int numKeyElements;							
								ss>>numKeyElements;
								while(numKeyElements--){
									int thiskey;
									ss>>thiskey;
									keys[(unsigned int)thiskey] |= 0x80;
								}

								mKeybdClicked=TRUE;
								DataReceived=TRUE;
								break;
							}							
						default:
							break;
					}
				}
			}

			//time line:
			//|0||1||<........>||d1+2||<........>|
			//|r||r||r||r||r||r||r||r||r||r||r||r|
			//|ACT1||<-delay1->||ACT2||<-delay2->|
			if(TRUE == MacroEnabled){
				BYTE* keys = static_cast<BYTE*>(data);
				if(0 == MacroDelayCounter)	{
					//act1 do
					for (auto it= KeyGroup1.begin(),end = KeyGroup1.end(); it!= end; ++it){
						keys[(int)*it] |= 0x80;
					}
				}else if( 1 == MacroDelayCounter){
					//act1 clr
					for (auto it= KeyGroup1.begin(),end = KeyGroup1.end(); it!= end; ++it){
						keys[(int)*it] &= ~0x80;
					}
				}else if ( delay1+2 == MacroDelayCounter){
					//act2 do
					for (auto it= KeyGroup2.begin(),end = KeyGroup2.end(); it!= end; ++it){
						keys[(int)*it] |= 0x80;
					}
				}else if ( delay1+3 == MacroDelayCounter){
					//act2 clr
					for (auto it= KeyGroup2.begin(),end = KeyGroup2.end(); it!= end; ++it){
						keys[(int)*it] &= ~0x80;
					}
				}
				DataReceived=TRUE;
				MacroDelayCounter++;
				if (MacroDelayCounter > delay1 +delay2 +3 ){
					MacroDelayCounter =0;
				}
			}

			if(TRUE == m_serverPipe->IsBroken()){
				delete m_serverPipe;
				m_serverPipe = new NamedPipeServer(KEYBDPIPENAME);
			}
		}

		if(TRUE == DataReceived){
			return DI_OK;
		}
		return hr;
	}

	STDMETHOD(GetDeviceData)(DWORD size, LPDIDEVICEOBJECTDATA data, LPDWORD numElements, DWORD flags)
	{
		HRESULT hr= m_device->GetDeviceData(size, data, numElements, flags);
		
		return hr;

	}

	STDMETHOD(SetDataFormat)(LPCDIDATAFORMAT df)
	{		
		return m_device->SetDataFormat(df);
	}

	STDMETHOD(SetEventNotification)(HANDLE event)
	{
		return m_device->SetEventNotification(event);
	}

	STDMETHOD(SetCooperativeLevel)(HWND window, DWORD level)
	{
		return m_device->SetCooperativeLevel(window, level);
	}

	STDMETHOD(GetObjectInfo)(LPDIDEVICEOBJECTINSTANCE object, DWORD objId, DWORD objHow)
	{
		return m_device->GetObjectInfo(object, objId, objHow);
	}

	STDMETHOD(GetDeviceInfo)(LPDIDEVICEINSTANCE di)
	{
		return m_device->GetDeviceInfo(di);
	}

	STDMETHOD(RunControlPanel)(HWND owner, DWORD flags)
	{
		return m_device->RunControlPanel(owner, flags);
	}

	STDMETHOD(Initialize)(HINSTANCE instance, DWORD version, REFGUID rguid)
	{
		return m_device->Initialize(instance, version, rguid);
	}

	STDMETHOD(CreateEffect)(REFGUID rguid, LPCDIEFFECT effect_params, LPDIRECTINPUTEFFECT* effect, LPUNKNOWN unknown)
	{
		return m_device->CreateEffect(rguid, effect_params, effect, unknown);
	}

    STDMETHOD(EnumEffects)(LPDIENUMEFFECTSCALLBACK callback, LPVOID ref, DWORD type)
	{
		return m_device->EnumEffects(callback, ref, type);
	}

    STDMETHOD(GetEffectInfo)(LPDIEFFECTINFO effect_info, REFGUID rguid)
	{
		return m_device->GetEffectInfo(effect_info, rguid);
	}

    STDMETHOD(GetForceFeedbackState)(LPDWORD state)
	{
		return m_device->GetForceFeedbackState(state);
	}

    STDMETHOD(SendForceFeedbackCommand)(DWORD flags)
	{
		return m_device->SendForceFeedbackCommand(flags);
	}

    STDMETHOD(EnumCreatedEffectObjects)(LPDIENUMCREATEDEFFECTOBJECTSCALLBACK callback, LPVOID ref, DWORD flags)
	{
		return m_device->EnumCreatedEffectObjects(callback, ref, flags);
	}

    STDMETHOD(Escape)(LPDIEFFESCAPE escape)
	{
		return m_device->Escape(escape);
	}

    STDMETHOD(Poll)()
	{

		HRESULT hr= m_device->Poll();
		//return DI_OK;
		return hr;
	}

	STDMETHOD(SendDeviceData)(DWORD size, LPCDIDEVICEOBJECTDATA data, LPDWORD num_elements, DWORD flags)
	{
		return m_device->SendDeviceData(size, data, num_elements, flags);
	}

	STDMETHOD(EnumEffectsInFile)(LPCTSTR file_name, LPDIENUMEFFECTSINFILECALLBACK callback, LPVOID ref, DWORD flags)
	{
		return m_device->EnumEffectsInFile(file_name, callback, ref, flags);
	}

    STDMETHOD(WriteEffectToFile)(LPCTSTR file_name, DWORD num_entries, LPDIFILEEFFECT effects, DWORD flags)
	{
		return m_device->WriteEffectToFile(file_name, num_entries, effects, flags);
	}

    STDMETHOD(BuildActionMap)(LPDIACTIONFORMAT format, LPCTSTR username, DWORD flags)
	{
		return m_device->BuildActionMap(format, username, flags);
	}

    STDMETHOD(SetActionMap)(LPDIACTIONFORMAT format, LPCTSTR username, DWORD flags)
	{
		return m_device->SetActionMap(format, username, flags);
	}

    STDMETHOD(GetImageInfo)(LPDIDEVICEIMAGEINFOHEADER image_header)
	{
		return m_device->GetImageInfo(image_header);
	}

private:
	IDirectInputDevice8* m_device;	
	NamedPipeServer *m_serverPipe;	
};